/**
*    Copyright (C) 2008  Chase Kernan 
*    chase.kernan@gmail.com
*
*    This program is free software: you can redistribute it and/or modify
*    it under the terms of the GNU General Public License as published by
*    the Free Software Foundation, either version 3 of the License, or
*    (at your option) any later version.
*
*    This program is distributed in the hope that it will be useful,
*    but WITHOUT ANY WARRANTY; without even the implied warranty of
*    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*    GNU General Public License for more details.
*
*    You should have received a copy of the GNU General Public License
*    along with this program.  If not, see <http://www.gnu.org/licenses/>.
* 
**/

package com.chasekernan.hxnova.client.mapviewer;

import com.chasekernan.hxnova.client.Selection;
import flash.display.Sprite;
import flash.filters.GlowFilter;
import flash.geom.Rectangle;
import com.chasekernan.hxnova.utils.IntVector;
import com.chasekernan.hxnova.utils.Vector;
import com.chasekernan.hxnova.core.dataholders.Global;
import com.chasekernan.hxnova.core.players.Player;

private typedef ScanPoint = {
    var location : Vector;
    var radius : Int;
}

class MapViewer extends Sprite {
    
    public static var EPSILON                  = 10.0;
    public static var EPSILON_SQUARED          = EPSILON * EPSILON;
    
    public static var DEFAULT_BACKGROUND_COLOR = 0x000000;//0x111111;
    public static var NORMAL_SCAN_COLOR        = 0x770000;//0x661c1c;
    public static var PENETRATING_SCAN_COLOR   = 0x444400;
    public static var GRID_COLOR               = 0xFFFFFF;
    public static var GRID_SPACING             = 100;
    public static var FRIENDLY_COLOR           = 0xcccc00;
    public static var NUETRAL_COLOR            = 0x999999;
    public static var ENEMY_COLOR              = 0xcc0000;
    public static var SELF_COLOR               = 0x0066ff;
    public static var SELECTION_COLOR          = 0x33ffff;
    public static var SELECTION_GLOW           = {
        new GlowFilter(SELECTION_COLOR, 1, 8, 8, 2, 2);
    };
    
    public var stars           : Array<StarObject>;
    private var starsHolder    : Sprite;
    
    private var fleetsHolder   : Sprite;
    public var fleets          : Array<FleetObject>;
    
    public var salvage         : Array<SalvageObject>;
    private var salvageHolder  : Sprite;
    
    public var pointOfView     : Player;
    
    private var background     : Sprite;
    private var grid           : Sprite;
    private var screenSize     : IntVector;
    private var options        : MapOptions;
    private var bounds         : Rectangle;
    private var normalScans    : Sprite;
    private var penScans       : Sprite;
    private var penMaskSprite  : Sprite;
    private var normMaskSprite : Sprite;
    private var oldTopLeft     : IntVector;
    
    public function new(screenSize : IntVector, pointOfView : Player) {
        super();
        
        if (pointOfView == null) throw "Point of view cannot be null.";
        this.pointOfView = pointOfView;
        
        setScreenSize(screenSize);
        
        background = new Sprite();
        addChild(background);
        
        normalScans = new Sprite();
        addChild(normalScans);   
        normMaskSprite = new Sprite();
        addChild(normMaskSprite);
        
        penScans = new Sprite();
        addChild(penScans);
        penMaskSprite = new Sprite();
        addChild(penMaskSprite);
        
        grid = new Sprite();
        addChild(grid);
        
        salvageHolder = new Sprite();
        addChild(salvageHolder);
        salvage = new Array();
        for (salv in Global.salvage) {
            var obj = new SalvageObject(salv);
            salvageHolder.addChild(obj);
            salvage.push(obj);
        }
        
        starsHolder = new Sprite();
        addChild(starsHolder);
        stars = new Array();
        for (star in Global.stars) {
            var obj = new StarObject(star);
            starsHolder.addChild(obj);
            stars.push(obj);
        }
        
        fleetsHolder = new Sprite();
        addChild(fleetsHolder);
        fleets = new Array();
        for (fleet in Global.fleets) {
            var obj = new FleetObject(fleet);
            fleetsHolder.addChild(obj);
            fleets.push(obj);
        }
        
        var me = this;
        Selection.addListener(function() {
            if (me.oldTopLeft == null) me.oldTopLeft = new IntVector(0, 0);
            me.render(me.oldTopLeft);
        });
    }
    
    public function setScreenSize(to : IntVector) : IntVector {
        if (to == null) throw "Screen size cannot be null.";
        
        screenSize = to.clone();
        return screenSize;
    }
    
    public function getScreenSize() : IntVector {
        return screenSize;
    }
    
    /**
        Renders the map with the top left point being a _game_ point.
        In order for the options to be omitted, [render] needs to have been 
        called before with the options given as it uses the previously set 
        options.
    **/
    public function render(topLeft : IntVector, ?options : MapOptions) {
        if (options == null) {
            if (this.options == null) {
                throw "Map options must have been set previously in order to" +
                      " omit them in future calls.";
            }
            options = this.options;
        } else {
            this.options = options;
        }
        
        oldTopLeft = topLeft.clone();
        
        bounds = new Rectangle(topLeft.x, topLeft.y, 
                               screenSize.x / options.zoom, 
                               screenSize.y / options.zoom);
        
        createBackground();
        if (options.drawScanners) {
            normalScans.visible = true;
            penScans.visible = true;
            handleScanners();
        } else {
            normalScans.visible = false;
            penScans.visible = false;
        }
        
        if (options.drawGrid) {
            grid.visible = true;
            renderGrid();
        } else {
            grid.visible = false;
        }
        
        renderStars();
        renderFleets();
        
        scaleX = options.zoom;
        scaleY = options.zoom;
        scrollRect = new Rectangle(0, 0, screenSize.x / options.zoom, 
                                         screenSize.y / options.zoom);
    }
    
    /**
        Returns all of the objects within [EPSILON] units of the given point.
        If there are no objects at the given point, then an empty array is 
        returned.
        
        NOTE: The point specified isn't relative to any point such as the 
        bounds given to the MapViewer when rendering. Instead it's a physical
        point _affected_ by zoom.
    **/
    public function getObjectsAtPoint(point : Vector, ?zoom = 1.0) 
                    : Array<IMapObject> {
        var objects = new Array<IMapObject>();
        
        for (obj in getStarsAtPoint(point, zoom)) objects.push(obj);
        for (obj in getFleetsAtPoint(point, zoom)) objects.push(obj);
        for (obj in getSalvageAtPoint(point, zoom)) objects.push(obj);
        
        return objects;
    }
    
    public function getAllObjects() : Array<IMapObject> {
        var objects = new Array<IMapObject>();
        
        for (obj in stars) objects.push(obj);
        for (obj in fleets) objects.push(obj);
        for (obj in salvage) objects.push(obj);
        
        return objects;
    }
    
    /**
        Returns all of the star objects within [EPSILON] units of the given
        point. If there are no stars at the given point, then an empty array is 
        returned.
        
        NOTE: The point specified isn't relative to any point such as the 
        bounds given to the MapViewer when rendering. Instead it's a physical
        point _affected_ by zoom.
    **/
    public function getStarsAtPoint(point : Vector, ?zoom = 1.0) 
                    : Array<StarObject> {
        var starObjects = new Array<StarObject>();
        for (obj in stars) {
            if (obj.star.location.getScale(zoom).distanceSquaredTo(point) <
                    EPSILON_SQUARED) {
                starObjects.push(obj);
            }
        }
        
        return starObjects;
    }
    
    /**
        Returns all of the fleet objects within [EPSILON] units of the given
        point. If there are no fleets at the given point, then an empty array is 
        returned.
        
        NOTE: The point specified isn't relative to any point such as the 
        bounds given to the MapViewer when rendering. Instead it's a physical
        point _affected_ by zoom.
    **/
    public function getFleetsAtPoint(point : Vector, ?zoom = 1.0) 
                    : Array<FleetObject> {
        var fleetObjects = new Array<FleetObject>();
        for (obj in fleets) {
            if (obj.fleet.location.getScale(zoom).distanceSquaredTo(point) < 
                    EPSILON_SQUARED) {
                fleetObjects.push(obj);
            }
        }
        
        return fleetObjects;
    }
    
    /**
        Returns all of the salvage objects within [EPSILON] units of the given
        point. If there is no salvage at the given point, then an empty array 
        is returned.
        
        NOTE: The point specified isn't relative to any point such as the 
        bounds given to the MapViewer when rendering. Instead it's a physical
        point _affected_ by zoom.
    **/
    public function getSalvageAtPoint(point : Vector, ?zoom = 1.0) 
                    : Array<SalvageObject> {
        var salvageObjects = new Array<SalvageObject>();
        for (obj in salvage) {
            if (obj.salvage.location.getScale(zoom).distanceSquaredTo(point) < 
                    EPSILON_SQUARED) {
                salvageObjects.push(obj);
            }
        }
        
        return salvageObjects;
    }
    
    private inline function renderSelection(obj : IMapObject) {
        untyped obj.filters = if (Selection.isSelected(obj)) [SELECTION_GLOW]
                              else                          [];
    }
    
    private inline function renderStars() {
        for (obj in stars) {
            if (!bounds.contains(obj.star.location.x, obj.star.location.y)) {
                obj.visible = false;
                continue;
            }
            
            obj.visible = true;
            obj.draw(bounds, pointOfView, options);
            
            renderSelection(obj);
        }
    }
    
    private inline function renderFleets() {
        for (obj in fleets) {
            if (!bounds.contains(obj.fleet.location.x, obj.fleet.location.y)) {
                obj.visible = false;
                continue;
            }
            
            obj.visible = true;
            obj.draw(bounds, pointOfView, options);
            
            renderSelection(obj);
        }
    }
    
    private inline function createBackground() {
        background.graphics.clear();
        background.graphics.beginFill(DEFAULT_BACKGROUND_COLOR);
        background.graphics.drawRect(0, 0, screenSize.x / options.zoom, 
                                           screenSize.y / options.zoom);
        background.graphics.endFill();
    }
    
    private inline function renderGrid() {
        grid.graphics.clear();
        grid.graphics.lineStyle(0.25, GRID_COLOR, 0.1);
        
        var fixedScreenWidth = screenSize.x / options.zoom;
        var fixedScreenHeight = screenSize.y / options.zoom;
        
        for (i in 1...Math.ceil(fixedScreenWidth / GRID_SPACING)) {
            grid.graphics.moveTo(i * GRID_SPACING, 0);
            grid.graphics.lineTo(i * GRID_SPACING, fixedScreenHeight);
        }
                
        for (j in 1...Math.ceil(fixedScreenHeight / GRID_SPACING)) {
            grid.graphics.moveTo(0, j * GRID_SPACING);
            grid.graphics.lineTo(fixedScreenWidth, j * GRID_SPACING);
        }
    }
    
    private function renderScans(points : Array<ScanPoint>, normal : Bool) {
        var maskSprite = if (normal) normMaskSprite else penMaskSprite;
        
        var colorSprite = if (normal) normalScans else penScans;
        colorSprite.graphics.clear();
        colorSprite.graphics.beginFill(if (normal) NORMAL_SCAN_COLOR else 
                                                   PENETRATING_SCAN_COLOR);
        colorSprite.graphics.drawRect(0, 0, screenSize.x / options.zoom, 
                                            screenSize.y / options.zoom);
        colorSprite.graphics.endFill();
        
        maskSprite.graphics.clear();
        maskSprite.graphics.lineStyle(0);
        for (point in points) {
            //if (!bounds.contains(point.location.x, point.location.y)) continue;
            maskSprite.graphics.beginFill(0xFFFFFF);
            maskSprite.graphics.drawCircle(point.location.x - bounds.x, 
                                           point.location.y - bounds.y, 
                                           point.radius);
            maskSprite.graphics.endFill();
        }
        
        colorSprite.mask = maskSprite;
        maskSprite.visible = false;
    }
    
    private function handleScanners() {
        //mostly a copy from [Scanning]
        //TODO: In [Scanning], instead of having 2 versions of the loop, 
        //just use a closure.
        
        var penPoints = new Array<ScanPoint>();
        var normalPoints = new Array<ScanPoint>();
        
        //add star points
        var scanner = pointOfView.starScanner;
        
        if (scanner != null) {
            var nRadius = scanner.range;
            var pRadius = scanner.penetratingRange;
        
            var func = 
            if (scanner.isPenetrating()) {
                function (location : Vector) {
                    penPoints.push( {radius : pRadius, 
                                     location : location.clone()} );
                    normalPoints.push( {radius : nRadius, 
                                        location : location.clone()} );
                }
            } else {
                function (location : Vector) {
                    normalPoints.push( {radius : nRadius, 
                                        location : location.clone()} );
                }
            };
            
            for (star in Global.stars) {
                if (star == null || star.owner.id != pointOfView.id || 
                        star.hasScanner == null || !star.hasScanner /*|| 
                        !bounds.contains(star.location.x, star.location.y)*/) {
                    continue;
                }
                
                func(star.location);
            }
        }
        
        //add fleet points
        for (fleet in Global.fleets) {
            if (fleet.owner.id != pointOfView.id /*|| 
                    !bounds.contains(fleet.location.x, 
                                     fleet.location.y)*/) {
                continue;
            }
            
            var pRadius = fleet.getPenetratingScanningRange();
            var nRadius = fleet.getNormalScanningRange();
            
            if (pRadius > 0) {
                penPoints.push( {radius : pRadius, 
                                 location : fleet.location.clone()} );
            }
            if (nRadius > 0) {
                normalPoints.push( {radius : pRadius, 
                                    location : fleet.location.clone()} );
            }
        }
        
        renderScans(normalPoints, true);
        renderScans(penPoints, false);
    }
}